Skip to content

v0.7.41: email fixes, MCP fixes, gemini 3.6 flash, GitHub runners fallback, mothership experience improvements, soft delete chats#5835

Merged
waleedlatif1 merged 37 commits into
mainfrom
staging
Jul 22, 2026
Merged

v0.7.41: email fixes, MCP fixes, gemini 3.6 flash, GitHub runners fallback, mothership experience improvements, soft delete chats#5835
waleedlatif1 merged 37 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

waleedlatif1 and others added 30 commits July 20, 2026 22:29
…hangs (#5798)

* fix(mcp): pin outbound connections to IPv4 to avoid unreachable-IPv6 hangs

Root cause of the MCP 'connecting forever' / tool-discovery timeouts in
production: SSRF pinning forces each outbound connection to a single resolved
IP, which strips Happy Eyeballs' IPv4 fallback. dns.lookup(verbatim) returns the
IPv6 address first for Cloudflare-fronted dual-stack hosts (Gauge, api.exa.ai),
so the connection was pinned to IPv6 — but the production app subnets have no
IPv6 egress (AWS NAT gateways are IPv4-only), so the pinned IPv6 connection
connects into a void and hangs until the 30s timeout. Intermittent because the
resolver rotates A/AAAA order; works on retry when it happens to pick IPv4.

This is why h1.1 (#5797) did not fix it (protocol-independent) and why it never
reproduced locally (dev machines have IPv6 egress). Empirically verified: pinning
only the IPv6 address times out; preferring the IPv4 address connects in ~600ms.

Fix: at both pinned-resolution sites (validateMcpServerSsrf and validateUrlWithDNS)
resolve all addresses and prefer an IPv4 one; IPv6-only hosts still pin their sole
address. No signature/threading changes; SSRF validation of the pinned IP is
unchanged.

* chore(mcp): trim inline comments on the IPv4-preference fix
…5799)

Same class of bug fixed for MCP in #5798: SSRF pinning forces a single resolved
IP, which strips Happy Eyeballs' IPv4 fallback. dns.lookup(verbatim) returns the
IPv6 address first for dual-stack hosts, so a database host or 1Password Connect
server that is dual-stack gets pinned to IPv6 — unreachable on IPv4-only egress
(AWS NAT gateways) and hangs until timeout.

validateDatabaseHost and validateConnectServerUrl now resolve all addresses and
prefer IPv4; IPv6-only hosts still pin their sole address. SSRF validation of the
pinned IP is unchanged (the selected address is the one validated and pinned).
…r icon sizes (#5802)

- Re-rasterize the email wordmark from vector at 4x with baked-in vertical clear space so it renders crisp and is no longer clipped at the top/bottom; size the header logo box to 68x41 to match.
- Normalize the footer social icons (X, LinkedIn, GitHub, Slack) to a uniform cap-height on 40x40 (2x) canvases so they read as the same visual size.
- Add display:block + border:0 to the linked social icons to avoid Outlook's inline-image gap and blue link border.
…/ deprecated red) (#5805)

* feat(models): sunset tiers for models mirroring blocks — legacy amber / deprecated red

* fix(models): legacy model copy + exclude retired from copilot menu

* fix(copilot): exclude retired models from blocks-metadata fallback menu
…angs (#5807)

* fix(mcp): bound OAuth callback steps and log each phase to pinpoint hangs

The MCP OAuth callback could hang indefinitely after burning state, with no
error, no timeout, and no log — the request stalls somewhere between the burn
and token persistence with no observable I/O. Wrap each awaited callback step
(loadPreregisteredClient, mcpAuthGuarded, clearVerifier, discoverServerTools)
with a per-step timeout so a stalled operation surfaces as a labeled error
instead of hanging forever, and log start/done for every step, every guarded
OAuth fetch phase (validate/request/read-body), and the token-exchange DB
writes. The last start without a matching done names the exact stall point.

* fix(mcp): widen callback step bounds and drop resolved IP from fetch logs

- Raise mcpAuthGuarded step bound 60s->120s and discoverServerTools 30s->60s
  so a legitimately slow multi-leg exchange (each internal fetch is already
  30s-bounded) can't be falsely reported as failed.
- Log only a pinned boolean, not the resolved IP, so self-hosted private
  endpoint addresses don't reach log sinks.

* fix(mcp): tolerate non-thenable returns in callback timedStep helper

Promise.resolve(fn()) so a synchronously-returning (e.g. mocked) step can't
throw on .catch — restores the callback route unit tests.
…r accounts and custom bots (#5800)

* improvement(slack): merge slack_v2 auth into one credential picker for accounts and custom bots

* improvement(slack): label the Sim app group in the merged credential picker

* improvement(slack): move provider-specific picker copy and modal dispatch out of the credential selector
…d runners (#5808)

* chore(ci): add CI_PROVIDER toggle between Blacksmith and GitHub-hosted runners

* chore(ci): fail unrecognized CI_PROVIDER values over to GitHub-hosted runners
* improvement(mcp): let users re-open OAuth authorization anytime

A server stuck on 'OAuth Authorization required' had no discoverable way to
re-trigger the auth popup once it was closed or abandoned: the connect button
lived only in the server detail view and hard-disabled while 'Connecting…',
a state cleared by polling popup.closed (unreliable under COOP), so it could
stay locked until the 10-minute safety timeout.

- Add an 'Authorize'/'Reopen authorization' action to the server list row menu
  for unconnected OAuth servers, so re-auth is reachable without drilling in.
- Make the detail-view button always clickable (reopens a fresh popup) instead
  of locking on an unverifiable 'Connecting…' state.

* fix(mcp): guard OAuth start against double-click opening two popups

Now that the connect button stays clickable, block re-entry while the
/oauth/start request is in flight; cleared once it settles so a later reopen
still starts a fresh flow.

* test(mcp): cover OAuth popup double-click guard and reopen-after-settle

* fix(mcp): kill prior popup poll before reopening so it can't clear the new flow

Addresses a stale-poll race: an abandoned attempt's popup.closed interval could
fire mid-reopen and clear 'Connecting…' for the fresh flow.

* fix(mcp): retire prior OAuth flow's pending entry on reopen, not just its poll

Drop the previous attempt's pending-flow map entry and safety timeout up front
too, so a late BroadcastChannel result (settleFlow) can't clear the reopened
flow's connecting state during the new /oauth/start await.

* fix(mcp): preserve prior OAuth flow until reopen's start succeeds

Reworks reopen concurrency so a superseded or failed reopen can't lose a real
authorization:
- Retire the prior flow only after the replacement /oauth/start succeeds; a
  failed reopen keeps it so its popup can still complete and be honored.
- Guard the connecting-clear (settleFlow + popup poll) behind the in-flight
  start set, so a stale settle can't flicker the reopened flow's label.
- Invalidate server queries on already_authorized so the UI reflects a server
  that authorized out from under the client.
Adds a test for the already_authorized invalidation.

* refactor(mcp): make OAuth connecting state deterministic via reference counting

Replaces the scattered, race-prone stopConnecting/set-based tracking with a
per-server attempt count: each start increments once and clears exactly once
(fail, already_authorized, settle, or supersede), so the 'Connecting…' /
'Reopen authorization' label can never get stuck or flicker across concurrent
reopens. Retire prior flows whenever a replacement start succeeds (including
already_authorized); a failed reopen keeps the prior flow so it can still
complete. Drops the unreliable popup.closed poll entirely — the label already
invites reopening and the safety timeout guarantees clearing. Adds a test for
the failed-reopen-preserves-prior-flow case.
- Pin ChipModalTabs to w-fit so the segmented pill always hugs its tabs
  instead of stretching full-width when dropped into a flex column
- Migrate the mothership settings tab bar off a hand-rolled full-width
  underline bar onto ChipModalTabs
- Fix undefined --border-secondary token (fell back to currentColor) in
  mothership -> --border, matching the admin settings precedent
…5816)

* fix(mcp): don't blank the MCP tools page when tool discovery fails

A tool-discovery error (one slow/failing server, e.g. a stalled transport)
replaced the entire server list with an error banner. Gate the full-list
replacement on serversError (the list genuinely failing to load) only; when the
servers loaded, always render the list — each row already surfaces its own
discovery state via toolsStateByServer, with a non-blocking notice above the
list. Restores graceful degradation so one bad server can't hide the others.

* fix(mcp): surface partial discovery failures per-row and in the notice

When one server succeeds and another fails, the aggregate toolsError is
suppressed (data exists), so the failure was hidden. Now: the notice renders on
ANY per-server discovery error (not just all-fail), and each failed row surfaces
its live discovery error instead of reading as '0 tools' before its stored
status catches up.
…rd millisecond-tolerant (#5790)

* fix(workflow-persistence): make persistMigratedBlocks' updated_at guard millisecond-tolerant

* improvement(sockets): state improvements

* address comments
…5811)

* fix(custom-blocks): render and execute custom blocks as agent tools

* fix(custom-blocks): address review findings on agent-tool execution

* fix(custom-blocks): move executor tool id out of the custom-tool namespace
* feat(access-control): per-group chat-deploy auth modes + polish

- Add a per-permission-group 'Auth modes chat deployments may use'
  control, mirroring the existing public-file-share auth-mode control,
  so admins can disable specific chat auth modes (Public, Password, ...)
- Enforce it server-side on chat create/update via validateChatDeployAuth
  (ChatDeployAuthNotAllowedError -> 403), and filter the chat deploy UI's
  Access-control options to the allowed set (keeping the saved mode visible)
- Remove the dead 'Template' deploy option (hideDeployTemplate had no consumer)
- Shrink access-control block permission icons (size-[9px]) so they no
  longer touch their tile borders; normalize the tiles to size-[16px]

* fix(access-control): gate copilot chat deploy + fix new-chat default auth

- Enforce validateChatDeployAuth on the copilot executeDeployChat path
  (it called performChatDeploy directly, bypassing the HTTP-route gate)
- Enforce on update only when the auth mode actually changes, so a
  grandfathered saved mode can be re-saved (e.g. title-only edits)
- Chat deploy UI now grandfathers only the saved mode, not the unsaved
  'public' default; snap a new chat off a disallowed default to the first
  allowed mode so it can't submit a value the server rejects

* fix(access-control): keep a saved SSO chat mode selectable when SSO env is off

Treat savedAuthType === 'sso' as SSO-enabled when building auth options so an
existing SSO chat isn't dropped and silently downgraded by the snap effect.
Mirrors the file-share modal.

* improvement(access-control): require at least one auth mode when enabled

Guard the chat-deploy and file-share auth-mode multiselects so the last
allowed mode can't be removed while the feature is enabled — an empty
allow-list would silently block every deployment/share. Disabling the
feature entirely stays the job of the Hide Chat toggle / Public Sharing
checkbox.

* fix(access-control): never leave a new chat with zero selectable auth modes

Surface SSO in the chat deploy options whenever the permission group
explicitly allows it (not only when the env flag is on or the chat is
saved as SSO), so a group whose only allowed mode is SSO can't produce an
empty option set that strands a new chat on a disallowed default. Also
drop a redundant trailing comment in the copilot deploy handler.
…rting at 16MB (#5810)

* fix(pii): mask offloaded large payloads chunk-by-chunk and retry transient mask failures

A block output past the 16MB inline materialization ceiling aborted the run
before masking even started: the redaction path hydrated the whole offloaded
value at once, and the pre-flight size assert fired on the manifest's total
byteSize. Large-array manifests now page one stored chunk at a time
(materialize -> mask -> re-store, rebuilt via the manifest writer with preview
derived from masked items), so peak heap stays ~one chunk regardless of payload
size. Single refs up to the 64MB durable cap hydrate with a raised budget and
run serially outside the concurrency pool.

Mask-batch chunk requests now retry transient failures (network errors,
408/429/5xx, honoring Retry-After) with jittered backoff, so a single ALB blip
or Presidio pod restart no longer fails a whole payload's redaction. Nested-ref
masking now runs the string pass before ref substitution, fixing a latent
double-mask when a masked nested value shrinks back inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): retry runtime timeouts and socket closes in mask-batch chunks

Verified end-to-end against a 26MB / 40k-record offloaded output: the
chunk-wise path masks it in ~54s on a single local Presidio worker where the
old path aborted at the 16MB ceiling. The exercise surfaced two more transient
error shapes the retry classifier missed — runtime-level request timeouts
(undici's default 300s headers timeout, Bun's TimeoutError) and mid-flight
socket closes — both of which previously failed the whole payload's redaction
on the first occurrence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): gate the spaCy fast path on entities the loaded models can produce

The registry's SpacyRecognizers claim every entity in Presidio's default
NER-model mapping — including PHONE_NUMBER/AGE/ID/EMAIL, which exist for
transformer de-identification backends and which no spaCy model can emit. The
NER_ENTITIES derivation trusted that claim, so any request naming PHONE_NUMBER
(present in nearly every redaction rule) silently forced the full spaCy pass
and the regex-only fast path never fired. Intersect the claimed set with the
entities the loaded models' actual NER labels map onto; the hard floor of core
NER entities is unchanged, and a future backend that genuinely emits phone
labels would re-gate automatically.

Verified live: PHONE_NUMBER-only requests take nlp=skip with span parity
against the full path, PERSON still forces NER, and a 26MB/40k-record
block-output redaction with the realistic entity set runs entirely on the
fast path (~3.2min vs ~15min projected full-NER on one worker).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): hydrate oversized-chunk manifests serially, not just single refs

Review finding: a manifest whose packer emitted a chunk past the inline
ceiling (one item larger than the chunk target) hydrates that chunk with the
raised 64MB budget inside the REF_CONCURRENCY pool, so several such manifests
could hydrate oversized blobs concurrently — the exact heap scenario the
serial path exists to prevent. The serial gate now covers any ref whose
hydration can exceed the inline ceiling: oversized single refs and manifests
containing an oversized chunk. Normally-chunked manifests stay pooled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): serialize oversized hydrations globally across nested redaction passes

Review finding: the serial gate was per-resolveReplacements invocation, so
nested oversized refs discovered inside different pooled parents each got
their own pool and could hydrate oversized blobs concurrently. A shared
promise-chain gate now threads through the options from the entry points, and
a reentrancy flag lets a gated ref's own nested oversized work run directly
instead of deadlocking on the hold. Covered by a cross-parent max-in-flight
assertion and a nested-oversized deadlock regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(pii): fail fast on a null mask-batch body; use sleep() in gate test

A 200 response with a null JSON body threw TypeError on the data.masked read,
which the retry classifier treats as transient — burning the full retry budget
on a deterministic shape failure. Null-guard the body so it throws the
non-retryable shape error immediately. Also swap the gate test's inline
setTimeout promise for sleep() to satisfy check:utils, which failed CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Empirically: our client's first connect to these servers consistently hangs to
our 30s timeout, then a retry connects in ~100ms (the server answers initialize
in ~120ms from a normal client — proven via A/B). Treating that request timeout
as a dead connection (isDeadConnectionError) retired the pooled connection, so
the next discovery reconnected and paid the stall again — a connect/stall/
reconnect churn loop, and the cause of the intermittent 'Failed to discover' /
disconnected states.

A streamable-HTTP request timeout aborts only that request's own POST stream;
the session stays healthy for the next request. So stop poisoning the lease on a
timeout (matches the MCP SDK, OpenCode, and LibreChat, none of which retire a
connection on a request timeout). Only a real transport close/404/reset retires.
…#5819)

* diag(mcp): per-request phase logging on the streamable-HTTP transport

Mirrors the OAuth guarded-fetch logging that isolated the /oauth/start body
stall. Logs each transport request (host+method), the response headers with
TTFB, and failures — so a first-connect stall shows whether it hangs before
response headers (connect/request) or after (the SDK's stream read), which
isolates the client-side first-connect stall we haven't yet root-caused.

* diag(mcp): include error name/code in transport failure log

Distinguishes abort vs connect-refused vs TLS vs timeout in the failure-phase
data.
#5821)

* fix(mcp): retire pooled connections after consecutive request timeouts

Circuit breaker for the half-open-transport gap left by #5817: a lone timeout
still keeps the session warm (retiring on every timeout caused the
connect/stall/reconnect churn), but two consecutive timeouts with no healthy
request in between retire the connection so a genuinely half-open transport
can't serve repeated 30s failures until the liveness ping catches it. A healthy
release resets the count. Matches the LibreChat pattern of counting
connection-level failures rather than reacting to one.

* fix(mcp): classify TimeoutError-named aborts as timeouts for the circuit breaker

AbortSignal.timeout / undici surface a DOMException named TimeoutError whose
message lacks 'timed out'; without this the breaker treated it as a healthy
release and reset the streak. Adds name-based detection (incl. cause) + test.
* fix(ci): make GitHub-hosted fallback actually build and test

* fix(ci): drop unused build-args passthrough so the heap ceiling can't be overridden

* chore(ci): trim comments to the non-obvious constraints

* fix(ci): build the app image on a larger runner in GitHub mode
…rd (#5823)

* fix(mcp): replace single-IP pinning with validate-at-connect SSRF guard

Swaps the MCP transport + OAuth guard from pin-one-resolved-IP to the standard
pattern (LibreChat getSSRFConnect): DNS resolves normally and EVERY socket
connect filters the resolved addresses against the private/reserved blocklist,
closing the validate-then-trust window a rebind could race and removing the
non-standard mechanism implicated in the headers-then-no-body stalls (no
reference MCP client pins IPs; a pinned attempt welded to a bad flow cannot
escape, while retries rotate via resolver round-robin and succeed).

Adversarially reviewed before shipping; both findings closed here:
- Redirects are now followed manually with per-hop validation: an IP-literal
  redirect target (which bypasses ANY connect-time lookup - Node skips the
  custom lookup for numeric hosts) is checked explicitly, closing a metadata-
  endpoint redirect hole the old pin also had.
- Custom request headers are dropped on cross-origin hops, so a redirect to a
  second attacker host cannot harvest configured auth headers.

Policy gating unchanged: the guard activates exactly where the pin did
(validateMcpServerSsrf non-null; allowlist mode / localhost-on-self-hosted stay
unguarded). Non-MCP consumers of the pinned fetch are untouched. 397 tests
incl. new rebinding, mixed-answer, IP-literal-redirect, and hop-cap coverage.

* fix(mcp): restore IPv4 preference and harden the guarded redirect follower

- Restore validateUrlWithDNS's prefer-IPv4 resolution (a stale working-tree hunk
  accidentally reverted #5798 for the still-pinned non-MCP consumers).
- Validate the INITIAL url's IP-literal in followRedirectsGuarded, not just
  redirect hops, so the exported guard is self-contained.
- Drop entity headers (content-length/type/encoding) when a 301/302/303 switches
  a POST to a bodyless GET, which undici would otherwise reject.
- Lift method/headers/body/signal from a Request input instead of silently
  downgrading a guarded POST Request to a bare GET.

* fix(mcp): annotate headers double-cast and cancel redirect body before throw paths

- Add the missing double-cast-allowed annotation on the sanitized-headers cast
  (strict boundary audit).
- Cancel the redirect response body before the hop-cap / blocked-target throws
  so those paths can't leave a socket checked out on the long-lived Agent.

* fix(mcp): keep self-hosted private-resolving hosts unguarded (old-pin parity)

A DNS alias resolving to loopback/private is only reachable on self-hosted,
where the policy explicitly permits it; the guarded lookup would filter the
address and strand the connect where the old pin connected. Both MCP gates
(transport + OAuth guard) now route private/loopback resolutions over the
unguarded path, same as the localhost carve-out. Test IPs moved off RFC-5737
TEST-NET (which is correctly classified reserved).

* fix(mcp): pin (not skip) self-hosted private resolutions; refuse cross-origin body forwards

- The private/loopback carve-out now keeps the LEGACY PIN to the validated
  address instead of falling back to unguarded fetch — preserving both the old
  behavior and its anti-rebinding property for self-hosted DNS aliases.
- Cross-origin redirect hops now also refuse to forward a request body (307/308
  preserve method+body; post-pin those redirects really dial the new origin, so
  an open redirect could exfiltrate OAuth client secrets). Bodyless cross-origin
  redirects still follow. Tests for both.

* chore(mcp): true up stale pinned-era doc comments
…est (#5824)

* fix(mcp): open the OAuth popup synchronously and bound the start request

Two bugs behind 'pressed Connect/Reopen and nothing happened':
- window.open ran AFTER awaiting /oauth/start, outside the browser's user
  activation, so the popup was silently blocked (worst on the add-server flow).
  Popup-first now: open about:blank synchronously in the click (named window,
  so a re-click focuses/reuses an existing authorization window), navigate it
  once the start returns; close it on failure/already_authorized; clear toast
  when genuinely blocked (and skip the request entirely).
- /oauth/start had no client timeout, so a stalled start held the re-entrancy
  guard and the connecting label indefinitely. Bounded at 30s; on timeout the
  popup closes, the label resets, and retry is immediately available.

* fix(mcp): harden popup-first reopen edges

- Retire prior flows as soon as the named popup opens (the open already blanked
  any prior auth window; a failed start must not leave a windowless flow
  'connecting' for the 10-minute safety timeout).
- Check the COOP-fallback window.open result; when blocked, clear the state and
  toast instead of registering a windowless pending flow.
- Feature-detect AbortSignal.timeout (Safari <16) with an AbortController
  fallback for the bounded /oauth/start.

* fix(mcp): close the blank popup when post-start navigation fails

* chore(mcp): correct connecting-count ordering comment
…istent (#5829)

* improvement(mcp): make the OAuth experience visible and verbally consistent

From a full UX-consistency audit of the MCP settings surfaces:
- One name for one action: 'Authorize' / 'Reopen authorization' everywhere
  (list chip, row, detail) — was three different labels across surfaces.
- The connecting state is now visible: the row subtitle shows a muted
  'Waiting for authorization...' instead of continuing to shout the red
  'OAuth authorization required' mid-flow.
- The Authorize affordance is a visible chip on the list row (was buried in
  the overflow menu), so a blocked popup's 'retry' has an obvious target.
- Removed the redundant aggregate discovery banner — every failing row
  already reports its specific error; two red messages for one failure.
- The header Refresh chip no longer renders an error sentence as its label
  (short 'Failed'; the Status field carries the explanation).
- Sentence-case sweep (Unnamed server, Not connected, Server name, Add MCP
  server / Edit MCP server, Add server, Test connection, Edit form, Delete
  MCP server), 'Search servers...' placeholder, row-subtitle/error text on
  the canonical tokens, and a Loading empty state instead of a blank flash.

* fix(mcp): show stale-discovery failures, best-effort popup-close label clear, drop redundant branch

- A failing latest discovery now shows its error even when cached tools exist
  (the stale tool count silently hid it).
- Best-effort popup.closed poll clears only the 'Waiting for authorization...'
  label share; the flow entry stays registered so a completion that still
  arrives over the BroadcastChannel is honored (settleFlow skips the
  double-decrement). Under COOP misreport the worst case is an early label
  reset, never a dropped completion.
- Remove the OAuth refresh-state branch made redundant by the 'Failed' change,
  plus its now-unused authType/error inputs.
TheodoreSpeaks and others added 4 commits July 21, 2026 22:11
…aming scroll (#5828)

* improvement(mothership): stable thinking indicator and jump-free streaming scroll

* fix(mothership): suppress shimmer over executing tool rows and seed chase interrupt baseline

* fix(mothership): keep shimmer mounted through the slot collapse so it animates out

* improvement(mothership): tighten transcript bottom padding, timed slot-exit latch, cleanup pass

* fix(mothership): bridge hidden special-tag streaming with the shimmer

* fix(mothership): hold sizer floor through reveal and reset chase deadline on park

* improvement(mothership): swap actions into the thinking slot at settle, quicken the chase
)

* feat(library): Best AI Agents for Data Extraction and RAG in 2026

* fix(library): correct CSV import claims, fix comparison link, drop duplicate FAQ body

* fix(library): remove unsupported GDPR compliance claim

* improvement(library): update connector source count to match registry (50+)

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
…#5833)

* fix(mcp): follow tools/list pagination instead of silently truncating

The SDK's listTools() returns a single page; a server that paginates via
nextCursor was silently truncated to page one. Follow the cursor bounded by
four independent budgets (50 pages / 1000 tools / 5 MB / 60s aggregate
wall-clock) plus a repeated-cursor guard — a page cap alone can't stop a server
returning a fresh cursor with no new tools. Partial results from earlier pages
are kept when a later page fails; only a page-one failure throws. Matches the
LibreChat capped-cursor-loop pattern; caps live in MCP_CLIENT_CONSTANTS.

* fix(mcp): count UTF-8 bytes, keep empty partials, and log page-cap accurately

Review fixes on the tools/list pagination loop:
- Buffer.byteLength(..., 'utf8') instead of .length so non-ASCII schemas can't
  overshoot the 5 MB budget by counting UTF-16 code units.
- Track pagesFetched (not tools.length) for the partial-success decision, so a
  valid-but-empty first page followed by a failing page returns [] instead of
  throwing and marking the server unhealthy.
- Explicit reachedEnd/page-cap distinction so a natural finish on exactly
  MAX_PAGES isn't mislogged as truncated.
…ted (#5830)

* feat(chat): soft-delete sidebar chats with restore from Recently Deleted

* fix(chat): review round 1 — restore workspace authz, purge recheck, archived-list invalidation

* test(chat): update SSE handler assertions for workspaceLists invalidation

* fix(chat): bump updatedAt on restore, recheck retention cutoff in task cleanup

* fix(chat): drop explicit feedback delete in task cleanup — chat FK cascade covers it

* test(chat): cover restore route; guard legacy copilot delete from hard-deleting mothership chats

* chore: revert unintended bun.lock drift from worktree install

* fix(cleanup): recheck workflow archive cutoff on delete; export deletedAt in chat drain
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner July 22, 2026 03:17
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (212 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 22, 2026 6:33am

Request Review

@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches CI runner selection and Docker/cache paths (deploy reliability), chat deploy authorization (access control), and MCP OAuth callback timing; mothership delete/restore changes data lifecycle but is scoped with tests.

Overview
Adds a break-glass CI_PROVIDER repo variable so workflows can run on GitHub-hosted runners during Blacksmith outages, with shared composite actions for cache mounts (sticky disk vs actions/cache) and Docker builds (Blacksmith vs upstream buildx). Image and Next.js build jobs pick larger paid runners on GitHub where memory would OOM.

Mothership sidebar chats move from hard delete to soft delete (deletedAt), with list/fork/update paths ignoring deleted rows, a restore API, Recently Deleted Chats tab, and legacy copilot delete blocked for mothership type. Chat deploy APIs enforce permission-group allowed auth modes on create and when auth type changes (grandfathered unchanged modes).

MCP OAuth callback steps are time-bounded and logged; settings UI surfaces per-server discovery errors, inline Authorize, and clearer copy without blanking the server list on tool fetch failure. PII fixes spaCy NER detection so entities like PHONE_NUMBER don’t force a full NLP pass when regex-only is enough.

Mothership streaming UI refactors the turn-level thinking shimmer (contextual labels, fixed slot, actions swap on settle) and adds a scroll sizer floor to reduce jump while streaming. Smaller changes: Slack merged credential picker, 1Password Connect IPv4 pinning, model deprecated from sunset, syncLocalDraftFromServer relocated under workflow stores, and docs tweak for Slack status.

Reviewed by Cursor Bugbot for commit 93fbf58. Configure here.

)

* fix(auth): gate email-otp auto-signup behind DISABLE_EMAIL_SIGNUP

* fix(auth): also close otp auto-signup under DISABLE_REGISTRATION
…nsient failure (#5839)

* fix(mcp): keep last-known-good tools instead of flashing red on a transient failure

A connected server with cached tools was turning red 'Failed to discover' on a
single transient discovery probe blip, even though the stored status stayed
connected with tools — because useMcpToolsQuery dropped React Query's retained
data on error, and the row's showDiscoveryError fired over a populated server.

Now the aggregate keeps last-known-good tools across a failed refetch, and the
row only hard-reds when there are genuinely no tools to show. A persistent
failure still surfaces via the stored connectionStatus (gated by
MAX_CONSECUTIVE_FAILURES), matching how Claude Code / LibreChat / OpenCode /
VS Code handle a transient tools/list failure on a live connection.

Validated against those clients' source + the MCP spec before changing; this
supersedes the #5829 over-correction that showed the error even when tools
existed.

* fix(mcp): drop stale tools once a server is persistently failed, keep on transient

Sharpens the last-known-good behavior with the transient-vs-persistent distinction
the reference clients make: keep cached tools through a transient discovery failure
(stored status still healthy) so a populated server doesn't blank, but drop them
once the stored connectionStatus crosses its failure threshold to error/disconnected
— so the shared aggregate the workflow editor consumes stops offering a dead
server's stale tool schemas.
@waleedlatif1
waleedlatif1 merged commit 3ca1b65 into main Jul 22, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants